// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.ui;

import bats.Assets;
import bats.Bats;
import com.jme3.app.Application;
import com.jme3.app.state.AppStateManager;
import com.jme3.cursors.plugins.JmeCursor;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.controls.ListBox;
import de.lessvoid.nifty.screen.Screen;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MyString;
import jme3utilities.nifty.GuiScreenController;
import jme3utilities.ui.Hotkey;

/**
 * A GUI screen controller for the screen used to edit hotkey bindings. Use
 * setEnabled() to reveal or hide the screen.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class BindScreen
        extends GuiScreenController
        implements ActionListener {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(BindScreen.class.getName());
    // *************************************************************************
    // fields
    /**
     * which input mode to edit: set by enable()
     */
    private GameInputMode subjectMode = null;
    /**
     * input mode for this screen: set by constructor
     */
    final private BindInputMode inputMode;
    /**
     * the short name for this mode
     */
    final public static String name = "bind";
    // *************************************************************************
    // constructors

    /**
     * Instantiate a disabled screen.
     */
    public BindScreen() {
        super(name, "Interface/Nifty/screens/bind.xml", false);

        inputMode = new BindInputMode(this);
        JmeCursor cursor = Assets.loadCursor("default.cur", 0, 31);
        inputMode.setCursor(cursor);
        setListener(inputMode);

        assert !isEnabled();
        assert !isInitialized();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Activate this disabled screen for a specified input mode.
     *
     * @param mode which mode (not null)
     */
    public void activate(GameInputMode mode) {
        assert this.subjectMode == null : this.subjectMode;
        assert !isEnabled();
        assert mode != null;
        assert mode.isEnabled();

        mode.getGameMode().setEnabled(false);
        this.subjectMode = mode;
        setEnabled(true);
    }

    /**
     * Deactivate this screen and return to the game mode that was edited.
     */
    void deactivate() {
        assert isEnabled();
        assert subjectMode != null;
        assert !subjectMode.isEnabled();

        setEnabled(false);
        subjectMode.getGameMode().setEnabled(true);
        subjectMode = null;
    }

    /**
     * Test whether a hotkey is exempt from being edited.
     *
     * @param hotkey (not null)
     * @return false if subject to editing, true if exempt
     */
    static boolean isExempt(Hotkey hotkey) {
        assert hotkey != null;

        int keyCode = hotkey.keyCode();
        /*
         * Windows uses these hotkeys:
         *  KEY_LMETA to open the Windows menu
         *  KEY_NUMLOCK to toggle keypad key definitions
         * SimpleApplication pre-assigns these hotkeys:
         *  KEY_ESCAPE to quit the application
         *  KEY_F5 to toggle the stats display
         *  KEY_C to print camera coordinates
         *  KEY_M to print memory statistics
         * In addition, the Nifty GUI uses these hotkeys:
         *  KEY_TAB for navigation
         *  KEY_RETURN for selection
         *  KEY_SPACE to reset the GUI
         * And com.jme3.app.state.ScreenshotAppState uses:
         *  KEY_SYSRQ to save a screenshot
         * For now, avoid re-assigning those hotkeys.
         */
        switch (keyCode) {
            case KeyInput.KEY_LMETA:
            case KeyInput.KEY_NUMLOCK:
            case KeyInput.KEY_ESCAPE:
            case KeyInput.KEY_F5:
            case KeyInput.KEY_C:
            case KeyInput.KEY_M:
            case KeyInput.KEY_TAB:
            case KeyInput.KEY_RETURN:
            case KeyInput.KEY_SPACE:
            case KeyInput.KEY_SYSRQ:
                return true;
        }
        return false;
    }
    // *************************************************************************
    // AbstractAppState methods

    /**
     * Update this screen.
     *
     * @param simInterval seconds since previous update (&ge;0)
     */
    @Override
    public void update(float simInterval) {
        assert simInterval >= 0f : simInterval;
        assert isEnabled();
        super.update(simInterval);

        getActionBox().refresh();
        getHotkeyBox().refresh();
        updateButtonLabels();

        String modeStatus = String.format("Editing hotkey bindings for %s mode",
                subjectMode.getLongName());
        setStatusText("modeStatus", modeStatus);
    }
    // *************************************************************************
    // ActionListener methods

    /**
     * Process an action from the mouse or keyboard.
     *
     * @param actionString textual description of the action (not null)
     * @param ongoing true if the action is ongoing, otherwise false
     * @param ignored
     */
    @Override
    public void onAction(String actionString, boolean ongoing, float ignored) {
        /*
         * Ignore actions which are not ongoing.
         */
        if (!ongoing) {
            return;
        }
        logger.log(Level.INFO, "Got action {0}", MyString.quote(actionString));

        if (actionString.equals("close")) {
            deactivate();
            return;
        }
        /*
         * Split the action string into words.
         */
        String[] words = actionString.split("\\s+");
        assert words.length > 0;
        String verb = words[0];
        switch (verb) {
            case "bind":
                if (words.length == 1) {
                    bindSelected();
                    return;
                }
                break;

            case "load":
                if (words.length == 1) {
                    subjectMode.loadBindings();
                    return;
                }
                break;

            case "return":
                if (words.length == 1) {
                    deactivate();
                    return;
                }
                break;

            case "save":
                if (words.length == 1) {
                    Bats.saveBindings(subjectMode);
                    return;
                }
                break;

            case "select":
                if (words.length == 2) {
                    int keyCode = Integer.valueOf(words[1]);
                    selectHotkey(keyCode);
                    return;
                }
                break;

            case "unbind":
                if (words.length == 1) {
                    unbindSelectedHotkey();
                    return;
                }
                break;
        }

        logger.log(Level.WARNING, "Action {0} was not handled.",
                MyString.quote(actionString));
    }
    // *************************************************************************
    // GuiScreenController methods

    /**
     * Callback which Nifty invokes when the screen becomes enabled.
     *
     * @param nifty (not null)
     * @param screen (not null)
     */
    @Override
    public void bind(Nifty nifty, Screen screen) {
        assert nifty != null;
        assert screen != null;
        super.bind(nifty, screen);
        /*
         * Populate the action listbox.
         */
        ListBox<ActionItem> actionBox = getActionBox();
        actionBox.clear();
        Collection<String> actions = Actions.getNames();
        for (String actionName : actions) {
            ActionItem item = new ActionItem(actionName, subjectMode);
            actionBox.addItem(item);
        }
        /*
         * Populate the hotkey listbox, putting bound keys before unbound ones.
         */
        ListBox<HotkeyItem> hotkeyBox = getHotkeyBox();
        hotkeyBox.clear();
        populateHotkeyBox(true);
        populateHotkeyBox(false);
    }

    /**
     * Initialize this screen.
     *
     * @param stateManager (not null)
     * @param application (not null)
     */
    @Override
    public void initialize(AppStateManager stateManager,
            Application application) {
        assert !isInitialized();
        assert !isEnabled();

        inputMode.initialize(stateManager, application);
        super.initialize(stateManager, application);
    }

    /**
     * Alter the visibility of this screen.
     *
     * @param newState if true, make this screen visible; if false, hide it
     */
    @Override
    public void setEnabled(boolean newState) {
        inputMode.setEnabled(newState);
        super.setEnabled(newState);
    }
    // *************************************************************************
    // private methods

    /**
     * Bind the selected action to the selected hotkey.
     */
    private void bindSelected() {
        Hotkey hotkey = getSelectedHotkey();
        assert hotkey != null;
        String actionName = getSelectedAction();
        assert actionName != null;
        subjectMode.bind(actionName, hotkey);
    }

    /**
     * Find the list box item for a specified hotkey.
     *
     * @param keyCode which hotkey
     * @return the pre-existing instance, or null if item not found
     */
    private HotkeyItem findHotkeyItem(int keyCode) {
        ListBox<HotkeyItem> listBox = getHotkeyBox();
        List<HotkeyItem> list = listBox.getItems();
        for (HotkeyItem item : list) {
            if (item.isForKeyCode(keyCode)) {
                return item;
            }
        }
        return null;
    }

    /**
     * Access the list box for selecting actions.
     *
     * @return the pre-existing instance (not null)
     */
    private ListBox<ActionItem> getActionBox() {
        Screen screen = getScreen();
        @SuppressWarnings("unchecked")
        ListBox<ActionItem> listBox =
                screen.findNiftyControl("actionList", ListBox.class);

        assert listBox != null;
        return listBox;
    }

    /**
     * Access the list box for selecting hotkeys.
     *
     * @return the pre-existing instance (not null)
     */
    private ListBox<HotkeyItem> getHotkeyBox() {
        Screen screen = getScreen();
        @SuppressWarnings("unchecked")
        ListBox<HotkeyItem> listBox =
                screen.findNiftyControl("hotkeyList", ListBox.class);

        assert listBox != null;
        return listBox;
    }

    /**
     * Access the selected action.
     *
     * @return the name of the selected action (or null if none selected)
     */
    private String getSelectedAction() {
        ListBox<ActionItem> listBox = getActionBox();
        List<ActionItem> selection = listBox.getSelection();
        if (selection.isEmpty()) {
            return null;
        }
        ActionItem item = selection.get(0);
        String actionName = item.getActionName();
        return actionName;
    }

    /**
     * Access the selected hotkey.
     *
     * @return the selected hotkey (or null if none selected)
     */
    private Hotkey getSelectedHotkey() {
        ListBox<HotkeyItem> listBox = getHotkeyBox();
        List<HotkeyItem> selection = listBox.getSelection();
        if (selection.isEmpty()) {
            return null;
        }
        HotkeyItem item = selection.get(0);
        Hotkey hotkey = item.getHotkey();
        return hotkey;
    }

    /**
     * Test whether an action is selected.
     *
     * @return true if an action is selected, false if none are selected
     */
    private boolean isActionSelected() {
        ListBox<ActionItem> listBox = getActionBox();
        List<ActionItem> selection = listBox.getSelection();
        boolean result = !selection.isEmpty();
        return result;
    }

    /**
     * Test whether a bound hotkey is selected.
     *
     * @return true if any bound hotkey is selected, false if no hotkeys are
     * selected or none of the selected hotkeys are bound
     */
    private boolean isBoundHotkeySelected() {
        ListBox<HotkeyItem> listBox = getHotkeyBox();
        List<HotkeyItem> selection = listBox.getSelection();
        for (HotkeyItem item : selection) {
            if (item.isBound()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Test whether a hotkey is selected.
     *
     * @return true if a hotkey is selected, false if none are selected
     */
    private boolean isHotkeySelected() {
        ListBox<HotkeyItem> listBox = getHotkeyBox();
        List<HotkeyItem> selection = listBox.getSelection();
        boolean result = !selection.isEmpty();
        return result;
    }

    /**
     * Add hotkeys to the appropriate list box.
     *
     * @param boundFlag if true, add bound hotkeys only; otherwise add unbound
     * hotkeys only
     */
    private void populateHotkeyBox(boolean boundFlag) {
        ListBox<HotkeyItem> listBox = getHotkeyBox();
        Collection<Hotkey> allHotkeys = Hotkey.getAll();
        for (Hotkey hotkey : allHotkeys) {
            if (isExempt(hotkey)) {
                continue;
            }
            if (subjectMode.binds(hotkey) == boundFlag) {
                HotkeyItem item = new HotkeyItem(hotkey, subjectMode);
                listBox.addItem(item);
            }
        }
    }

    /**
     * Alter the hotkey selection.
     *
     * @param keyCode
     */
    private void selectHotkey(int keyCode) {
        HotkeyItem item = findHotkeyItem(keyCode);
        assert item != null;

        ListBox<HotkeyItem> listBox = getHotkeyBox();
        listBox.setFocusItem(item);
        listBox.selectItem(item);
    }

    /**
     * Unbind the selected hotkey.
     */
    private void unbindSelectedHotkey() {
        Hotkey hotkey = getSelectedHotkey();
        assert hotkey != null;
        subjectMode.unbind(hotkey);
    }

    /**
     * Update the button labels in this screen.
     */
    private void updateButtonLabels() {
        String bindLabel = "";
        if (isHotkeySelected()
                && isActionSelected()
                && !isBoundHotkeySelected()) {
            bindLabel = "Bind";
        }
        setButtonLabel("bindButton", bindLabel);

        String returnLabel = String.format("Return to %s mode",
                subjectMode.getLongName());
        setButtonLabel("returnButton", returnLabel);

        String unbindLabel = "";
        if (isBoundHotkeySelected()) {
            unbindLabel = "Unbind hotkey";
        }
        setButtonLabel("unbindButton", unbindLabel);
    }
}